stringr no es parte del tidyverse, por lo que debemos instalarlo y cargarlo explícitamente.
library(tidyverse)
library(stringr)Podemos crear cadenas con comillas simples o comillas dobles. A diferencia de otros lenguajes, no hay diferencia en el comportamiento. Es recomendable usar siempre ", a menos que queramos crear una cadena que contenga múltiples comillas dobles.
string1 <- "Esto es una cadena"
string2 <- 'Si queremos incluir una "cita" dentro de una cadena, usamos comillas simples'Si olvidamos cerrar una cadena, veremos +, el caracter de continuación:
# > "Esta es una cadena sin comillas de cierre
# +
# +
# + AYUDA, ESTOY ATORADO!Si esto nos pasa, sólo debemos presionar Escape e intentar nuevamente.
Para incluir una comilla simple o doble literal en una cadena, podemos usar \ para “escaparla”:
double_quote <- "\"" # o '"'
single_quote <- '\'' # o "'"Esto significa que si deseamos incluir una barra invertida, tendremos que duplicarla: “\”.
Debemos tener en cuenta que la representación impresa de una cadena no es la misma que la cadena por si misma, esto se debe a que la representación impresa muestra los escapes. Para ver el contenido “crudo” de la cadena, usamos writeLines():
x <- c("\"", "\\")
x## [1] "\"" "\\"
writeLines(x)## "
## \
Hay un puñado de otros caracteres especiales. Los más comunes son "\n", nueva línea y "\t", tab, pero podemos ver la lista completa en la ayuda de ": ?'"' o ?"'". En ocasiones nos podemos encontrar con cadenas como "\u00b5" esta es una forma de escribir caracteres no alfabéticos que funcionan en todas las plataformas:
x <- "\u00b5"
x## [1] "µ"
Múltiples cadenas a menudo se almacenan en un vector de caracteres, que podemos crear con c():
c("one", "two", "three")## [1] "one" "two" "three"
R base contiene muchas funciones para trabajar con cadenas pero las evitaremos porque pueden ser inconsistentes, lo que las hace difíciles de recordar. En su lugar usaremos funciones de stringr. Estas tienen nombres más intuitivos y todos comienzan con str_. Por ejemplo, str_length() nos devuelve la cantidad de caracteres en una cadena:
str_length(c("a", "Ciencia de Datos con R", NA))## [1] 1 22 NA
El prefijo común str_ es particularmente útil si utilizamos RStudio, ya que al escribir str_ activará la función autocompletar, lo que nos permite ver todas las funciones del stringr:
Para combinar dos o más cadenas, usamos str_c():
str_c("x", "y")## [1] "xy"
str_c("x", "y", "z")## [1] "xyz"
Podemos usar el argumento sep para controlar la forma en que se separan las cadenas de texto:
str_c("x", "y", sep = ", ")## [1] "x, y"
Como en la mayoría de las otras funciones en R, los valores faltantes son contagiosos. Si deseamos que se impriman como "NA", usamos str_replace_na():
x <- c("abc", NA)
str_c("|-", x, "-|")## [1] "|-abc-|" NA
str_c("|-", str_replace_na(x), "-|")## [1] "|-abc-|" "|-NA-|"
Como se muestra arriba, str_c() se vectoriza, y recicla automáticamente vectores más cortos a la misma longitud que el más largo:
str_c("prefix-", c("a", "b", "c"), "-suffix")## [1] "prefix-a-suffix" "prefix-b-suffix" "prefix-c-suffix"
Los objetos de longitud 0 se eliminan en “silencio”. Esto es particularmente útil en conjunto con if:
name <- "Ana"
time_of_day <- "días"
birthday <- FALSE
str_c(
"Buenos ", time_of_day, " ", name,
if (birthday) " y FELIZ CUMPLEAÑOS",
"."
)## [1] "Buenos días Ana."
Para colapsar un vector de cadenas en una sola cadena, usamos collapse:
str_c(c("x", "y", "z"), collapse = ", ")## [1] "x, y, z"
Podemos extraer partes de una cadena usando str_sub(). Además de la cadena, str_sub() necesita los argumentos start y end que dan la posición (inclusiva) de la subcadena:
x <- c("Apple", "Banana", "Pear")
str_sub(x, 1, 3)## [1] "App" "Ban" "Pea"
str_sub(x, -3, -1)## [1] "ple" "ana" "ear"
Debemos notar que str_sub() no fallará si la cadena es demasiado corta, solo regresará tanto como sea posible:
str_sub("a", 1, 5)## [1] "a"
También podemos usar la forma de asignación de str_sub() para modificar cadenas:
str_sub(x, 1, 1) <- str_to_lower(str_sub(x, 1, 1))
x## [1] "apple" "banana" "pear"
Cambiar de mayúsculas a minúsculas es más complicado de lo que podría parecer a primera vista, ya que los diferentes idiomas tienen diferentes reglas para cambiar de mayúscula a minúscula. Podemos elegir qué conjunto de reglas usar al especificar una configuración regional:
# El Turco tiene dos i's: con y sin punto y tiene
# una regla diferente para pasarlas a mayúsculas:
str_to_upper(c("i", "ı"))## [1] "I" "I"
str_to_upper(c("i", "ı"), locale = "tr")## [1] "İ" "I"
La configuración regional se especifica como un código de idioma ISO 639, que es una abreviación de dos o tres letras. Podemos consultar la lista de Wikipedia para conocer el código de distintos idiomas, incluído el nuestro. Si dejamos la configuración regional en blanco, usará la configuración regional actual, tal como lo proporciona nuestro sistema operativo.
Otra operación importante que se ve afectada por la configuración regional es el ordenamiento. Las funciones de R base order() y sort() ordenan cadenas usando la configuración regional actual. Si deseamos un comportamiento robusto en diferentes computadoras, es posible que debamos utilizar str_sort() y str_order() que toman un argumento adicional locale:
x <- c("apple", "eggplant", "banana")
str_sort(x, locale = "en") # Inglés## [1] "apple" "banana" "eggplant"
str_sort(x, locale = "haw") # Hawaiano## [1] "apple" "eggplant" "banana"
Regexps es un lenguaje muy conciso que nos permite describir patrones en cadenas.
Para aprender expresiones regulares, usaremos str_view() y str_view_all(). Estas funciones toman un vector de caracteres y una expresión regular y muestran cómo coinciden. Comenzaremos con expresiones regulares muy simples y luego gradualmente lo complicaremos. Una vez que dominemos la coincidencia de patrones, aprenderemos cómo aplicar esas ideas con varias funciones de cadenas.
Los patrones más simples coinciden con cadenas exactas:
x <- c("apple", "banana", "pear")
str_view(x, "an")El siguiente paso en la complejidad es ., que coincide con cualquier caracter (excepto una nueva línea):
str_view(x, ".a.")Pero si “.” coincide con cualquier caracter, ¿cómo encontramos el caracter “.”? Debemos usar un “escape” para decirle a la expresión regular que deseamos coincidirlo exactamente en vez de usar su comportamiento especial. Al igual que las cadenas, las expresiones regulares usan la barra invertida, \, para escapar del comportamiento especial. Entonces, para que coincida con un ., necesitamos la expresión regular \.. Lamentablemente, esto crea un problema. Usamos cadenas para representar expresiones regulares, y \ también se usa como un símbolo de escape en cadenas. Entonces para crear la expresión regular \. necesitamos la cadena "\\.".
# Para crear la expresión regular, necesitamos \\
dot <- "\\."
# Pero la expresión sólo contiene un backslash:
writeLines(dot)## \.
# Con esto, le decimos a R que busque un . explícito
str_view(c("abc", "a.c", "bef"), "a\\.c")Si \ se usa como un carácter de escape en expresiones regulares, ¿cómo se combina un literal \? Necesitamos escapar, creando la expresión regular \\. Para crear esa expresión regular, necesitamos usar una cadena, que también necesita escapar \. Eso significa que para hacer coincidir un literal \ necesitamos escribir "\\\\" ¡necesitamos cuatro barras invertidas para que coincida con una!
x <- "a\\b"
writeLines(x)## a\b
str_view(x, "\\\\")A partir de ahora, escribiremos la expresión regular como \. y las cadenas que representan la expresión regular como "\\.".
Por defecto, las expresiones regulares coincidirán con cualquier parte de una cadena. A menudo es útil anclar la expresión regular para que coincida desde el principio o el final de la cadena. Podemos usar:
^ para que coincida con el comienzo de la cadena.
$ para que coincida el final de la cadena.
x <- c("apple", "banana", "pear")
str_view(x, "^a")str_view(x, "a$")Para recordar cuál es cuál, prueba esta mnemotecnia de Evan Misshula: si comienzas con potencia (^), terminas con dinero ($).
Para forzar una expresión regular a que sólo coincida con una cadena completa, debemos anclarla con ambos ^ y $:
x <- c("apple pie", "apple", "apple cake")
str_view(x, "apple")str_view(x, "^apple$")También podemos coincidir el límite entre las palabras con \b.
Hay una serie de patrones especiales que coinciden con más de un caracter. Ya vimos ., que coincide con cualquier caracter que no sea una nueva línea. Hay otras cuatro herramientas útiles:
\d: coincide con cualquier dígito.
\s: coincide con cualquier espacio en blanco.
[abc]: coincide con a, b, o c.
[^abc]: coincide con cualquier cosa excepto a, b, o c.
Recordemos que para crear una expresión regular que contenga \d o \s, debemos escapar \ de la cadena, por lo que escribiremos "\\d" o "\\s".
Una clase de caracteres que contenga un solo carácter es una buena alternativa a los escapes de barra invertida cuando se desea incluir un metacarácter único en una expresión regular. Muchas personas lo encuentran más legible.
# Buscar un caracter que normalmente tiene un significado especial en regex
str_view(c("abc", "a.c", "a*c", "a c"), "a[.]c")str_view(c("abc", "a.c", "a*c", "a c"), ".[*]c")str_view(c("abc", "a.c", "a*c", "a c"), "a[ ]")Esto funciona para la mayoría (pero no todos) de los metacaracteres de expresiones regulares: $ . | ? * + ( ) [ {. Desafortunadamente, algunos caracteres tienen un significado especial incluso dentro de una clase de caracteres y deben manejarse con escapes de barra invertida: ] \ ^ y -.
Podemos usar la alternancia para elegir uno o más patrones alternativos. Por ejemplo, abc|d..f coincidirá con "abc" o "deaf". Notemos que la precedencia para | es baja, por lo que abc|xyz coincide abc o xyz no abcyz o abxyz. Al igual que con las expresiones matemáticas, si la precedencia se vuelve confusa, usamos paréntesis para dejar en claro lo que deseamos:
str_view(c("grey", "gray"), "gr(e|a)y")El siguiente paso en complejidad implica controlar cuántas veces coincide un patrón:
?: 0 o 1
+: 1 o más
*: 0 o más
x <- "1888 es el año más largo en números romanos: MDCCCLXXXVIII"
str_view(x, "CC?")str_view(x, "CC+")str_view(x, 'C[LX]+')Notemos que la precedencia de estos operadores es alta, por lo que podemos escribir: colou?r para que coincida con la ortografía estadounidense o británica, por ejemplo. Eso significa que la mayoría de los usos necesitarán paréntesis, como bana(na)+.
También podemos especificar el número de coincidencias con precisión:
{n}: exactamente n{n,}: n o más{,m}: a lo sumo m{n,m}: entre n y mstr_view(x, "C{2}")str_view(x, "C{2,}")str_view(x, "C{2,3}")Por defecto, estas coincidencias son “codiciosas”: coincidirán con la cadena más larga posible. Podemos hacerlas “flojas”, haciendo coincidir la cadena más corta posible poniendo un ? después de ellas. Esta es una característica avanzada de las expresiones regulares, pero es útil saber que existe:
str_view(x, 'C{2,3}?')str_view(x, 'C[LX]+?')Anteriormente, aprendimos que los paréntesis son una forma de desambiguar expresiones complejas. Los paréntesis también crean un grupo de captura numerado (número 1, 2, etc.). Un grupo de captura almacena la parte de la cadena que coincide con la parte de la expresión regular dentro de los paréntesis. Podemos consultar textos anteriores con un grupo de captura con referencias, como \1, \2, etc. Por ejemplo, la siguiente expresión regular encuentra todas las frutas que tienen un par de letras repetidas.
str_view(fruit, "(..)\\1", match = TRUE)(Pronto, veremos cómo son útiles junto con str_match().)
Ahora que conocemos los conceptos básicos de las expresiones regulares, es hora de aprender cómo aplicarlas a problemas reales. Aprenderemos funciones de stringr que nos permiten:
Para determinar si un vector de caracteres coincide con un patrón, usamos str_detect(). Devuelve un vector lógico con la misma longitud que la entrada:
x <- c("apple", "banana", "pear")
str_detect(x, "e")## [1] TRUE FALSE TRUE
Recordemos que cuando se utiliza un vector lógico en un contexto numérico, FALSE se convierte en 0 y TRUE se convierte en 1. Esto hace que sum() y mean() sean utiles si deseamos responder preguntas sobre coincidencias en un vector más grande:
# ¿Cuántas palabras empiezan con t?
sum(str_detect(words, "^t"))## [1] 65
# ¿Qué proporción de palabras terminan en vocal?
mean(str_detect(words, "[aeiou]$"))## [1] 0.2765306
Cuando tenemos condiciones lógicas complejas (por ejemplo, coincidir con a o b, pero no con c a menos que d) a menudo es más fácil combinar múltiples llamadas de str_detect() con operadores lógicos, en lugar de tratar de crear una única expresión regular. Por ejemplo, aquí hay dos formas de encontrar todas las palabras que no contienen vocales:
# Encuentra todas las palabras que contengan al menos una vocal, y niegalas
no_vowels_1 <- !str_detect(words, "[aeiou]")
# Encuentra todas las palabras que estén compuestas por puras consonantes
no_vowels_2 <- str_detect(words, "^[^aeiou]+$")
identical(no_vowels_1, no_vowels_2)## [1] TRUE
Los resultados son idénticos, pero el primer enfoque es significativamente más fácil de entender. Si una expresión regular se vuelve demasiado complicada, debemos intentar dividirla en partes más pequeñas, dándole un nombre a cada pieza y luego combinando las piezas con operaciones lógicas.
Un uso común de str_detect() es seleccionar los elementos que coinciden con un patrón. Podemos hacer esto con un subconjunto lógico o el str_subset() conveniente :
words[str_detect(words, "x$")]## [1] "box" "sex" "six" "tax"
str_subset(words, "x$")## [1] "box" "sex" "six" "tax"
Normalmente, sin embargo, las cadenas serán una columna de un data frame, y en su lugar preferiremos usar filter:
df <- tibble(
word = words,
i = seq_along(word)
)
df %>%
filter(str_detect(words, "x$"))Una variación de str_detect() es str_count(): en lugar de un simple sí o no, nos dice cuántas coincidencias hay en una cadena:
x <- c("apple", "banana", "pear")
str_count(x, "a")## [1] 1 3 1
# En promedio, ¿cuántas vocales por palabra?
mean(str_count(words, "[aeiou]"))## [1] 1.991837
Es natural usar str_count() con mutate():
df %>%
mutate(
vowels = str_count(word, "[aeiou]"),
consonants = str_count(word, "[^aeiou]")
)Debemos tener en cuenta que las coincidencias nunca se superponen. Por ejemplo, en "abababa", ¿cuántas veces coincidirá el patrón "aba"? Las expresiones regulares dicen dos, no tres:
str_count("abababa", "aba")## [1] 2
str_view_all("abababa", "aba")Muchas funciones de stringr vienen en pares: una función funciona con una sola coincidencia, y la otra funciona con todas las coincidencias. La segunda función tendrá el sufijo _all.
Para extraer el texto real de una coincidencia, usamos str_extract(). Para demostrarlo, vamos a necesitar un ejemplo más complicado. Usaremos las Oraciones de Harvard, que fueron diseñadas para probar sistemas VOIP, pero también son útiles para practicar expresiones regulares. Estas se encuentran en stringr::sentences:
length(sentences)## [1] 720
head(sentences)## [1] "The birch canoe slid on the smooth planks."
## [2] "Glue the sheet to the dark blue background."
## [3] "It's easy to tell the depth of a well."
## [4] "These days a chicken leg is a rare dish."
## [5] "Rice is often served in round bowls."
## [6] "The juice of lemons makes fine punch."
Imagina que queremos encontrar todas las oraciones que contienen un color. Primero creamos un vector de nombres de colores y luego lo convertimos en una única expresión regular:
colours <- c("red", "orange", "yellow", "green", "blue", "purple")
colour_match <- str_c(colours, collapse = "|")
colour_match## [1] "red|orange|yellow|green|blue|purple"
Ahora podemos seleccionar las oraciones que contienen un color, y luego extraer el color para descubrir cuál es:
has_colour <- str_subset(sentences, colour_match)
matches <- str_extract(has_colour, colour_match)
head(matches)## [1] "blue" "blue" "red" "red" "red" "blue"
Notemos que str_extract() solo extrae la primera coincidencia. Podemos ver eso más fácilmente al seleccionar primero todas las oraciones que tienen más de 1 coincidencia:
more <- sentences[str_count(sentences, colour_match) > 1]
str_view_all(more, colour_match)str_extract(more, colour_match)## [1] "blue" "green" "orange"
Este es un patrón común para las funciones de cadenas, porque trabajar con una sola coincidencia nos permite utilizar estructuras de datos mucho más simples. Para obtener todas las coincidencias, usa str_extract_all(). Devuelve una lista:
str_extract_all(more, colour_match)## [[1]]
## [1] "blue" "red"
##
## [[2]]
## [1] "green" "red"
##
## [[3]]
## [1] "orange" "red"
Si usamos simplify = TRUE, str_extract_all() devolverá una matriz con coincidencias cortas expandidas a la misma longitud que la más larga:
str_extract_all(more, colour_match, simplify = TRUE)## [,1] [,2]
## [1,] "blue" "red"
## [2,] "green" "red"
## [3,] "orange" "red"
x <- c("a", "a b", "a b c")
str_extract_all(x, "[a-z]", simplify = TRUE)## [,1] [,2] [,3]
## [1,] "a" "" ""
## [2,] "a" "b" ""
## [3,] "a" "b" "c"
En el ejemplo anterior, es posible que hayas notado que la expresión regular coincide con “flickered”, que no es un color. Modifica la expresión regular para solucionar el problema.
De los datos de las oraciones de Harvard, extrae:
ing.Ya sabemos que podemos hacer uso de el paréntesis para aclarar la precedencia y para referencias al momento de la comparación. También podemos usarlo para extraer partes de una coincidencia compleja. Por ejemplo, imagina que queremos extraer sustantivos de las oraciones. Como heurística, buscaremos cualquier palabra que venga después de "a" o "the". Definir una “palabra” en una expresión regular es un poco complicado, así que utilizaremos una aproximación simple: una secuencia de al menos un carácter que no es un espacio.
noun <- "(a|the) ([^ ]+)"
has_noun <- sentences %>%
str_subset(noun) %>%
head(10)
has_noun %>%
str_extract(noun)## [1] "the smooth" "the sheet" "the depth" "a chicken" "the parked"
## [6] "the sun" "the huge" "the ball" "the woman" "a helps"
str_extract() nos da la coincidencia completa, str_match() nos da a cada componente individual. En lugar de un vector de caracteres, devuelve una matriz, con una columna para la coincidencia completa seguida de una columna para cada grupo:
has_noun %>%
str_match(noun)## [,1] [,2] [,3]
## [1,] "the smooth" "the" "smooth"
## [2,] "the sheet" "the" "sheet"
## [3,] "the depth" "the" "depth"
## [4,] "a chicken" "a" "chicken"
## [5,] "the parked" "the" "parked"
## [6,] "the sun" "the" "sun"
## [7,] "the huge" "the" "huge"
## [8,] "the ball" "the" "ball"
## [9,] "the woman" "the" "woman"
## [10,] "a helps" "a" "helps"
(Como era de esperar, nuestra heurística para detectar sustantivos es pobre, y también capta adjetivos como smooth y parked).
Si tenemos los datos en un tibble, a menudo es más fácil de usar tidyr::extract(). Funciona de la misma manera str_match() pero requiere que nombremos las coincidencias, que luego se colocan en columnas nuevas:
tibble(sentence = sentences) %>%
tidyr::extract(
sentence, c("article", "noun"), "(a|the) ([^ ]+)",
remove = FALSE
)Como en str_extract(), si queremos todas las coincidencias para cada cadena, necesitaremos str_match_all().
str_replace() y str_replace_all() nos permiten reemplazar coincidencias con nuevas cadenas. El uso más simple es reemplazar un patrón con una cadena fija:
x <- c("apple", "pear", "banana")
str_replace(x, "[aeiou]", "-")## [1] "-pple" "p-ar" "b-nana"
str_replace_all(x, "[aeiou]", "-")## [1] "-ppl-" "p--r" "b-n-n-"
Con str_replace_all() podemos realizar reemplazos múltiples mediante el suministro de un vector nombrado:
x <- c("1 house", "2 cars", "3 people")
str_replace_all(x, c("1" = "one", "2" = "two", "3" = "three"))## [1] "one house" "two cars" "three people"
En lugar de reemplazar con una cadena fija, podemos usar referencias para insertar componentes de la coincidencia. Cambiemos la segunda por la tercera palabra:
sentences %>%
str_replace("([^ ]+) ([^ ]+) ([^ ]+)", "\\1 \\3 \\2") %>%
head(5)## [1] "The canoe birch slid on the smooth planks."
## [2] "Glue sheet the to the dark blue background."
## [3] "It's to easy tell the depth of a well."
## [4] "These a days chicken leg is a rare dish."
## [5] "Rice often is served in round bowls."
Usamos str_split() para dividir una cadena en pedazos. Por ejemplo, podríamos dividir oraciones en palabras:
sentences %>%
head(5) %>%
str_split(" ")## [[1]]
## [1] "The" "birch" "canoe" "slid" "on" "the" "smooth"
## [8] "planks."
##
## [[2]]
## [1] "Glue" "the" "sheet" "to" "the"
## [6] "dark" "blue" "background."
##
## [[3]]
## [1] "It's" "easy" "to" "tell" "the" "depth" "of" "a" "well."
##
## [[4]]
## [1] "These" "days" "a" "chicken" "leg" "is" "a"
## [8] "rare" "dish."
##
## [[5]]
## [1] "Rice" "is" "often" "served" "in" "round" "bowls."
Debido a que cada componente puede contener un número diferente de elementos, esto devuelve una lista. Si estamos trabajando con un vector de longitud 1, lo más fácil es extraer el primer elemento de la lista:
"a|b|c|d" %>%
str_split("\\|") %>%
.[[1]]## [1] "a" "b" "c" "d"
De lo contrario, al igual que las otras funciones de cadena que devuelven una lista, podemos usar simplify = TRUE para obtener una matriz:
sentences %>%
head(5) %>%
str_split(" ", simplify = TRUE)## [,1] [,2] [,3] [,4] [,5] [,6] [,7]
## [1,] "The" "birch" "canoe" "slid" "on" "the" "smooth"
## [2,] "Glue" "the" "sheet" "to" "the" "dark" "blue"
## [3,] "It's" "easy" "to" "tell" "the" "depth" "of"
## [4,] "These" "days" "a" "chicken" "leg" "is" "a"
## [5,] "Rice" "is" "often" "served" "in" "round" "bowls."
## [,8] [,9]
## [1,] "planks." ""
## [2,] "background." ""
## [3,] "a" "well."
## [4,] "rare" "dish."
## [5,] "" ""
También podemos pedir un número máximo de elementos:
fields <- c("Nombre: Ana", "País: México", "Edad: 29")
fields %>% str_split(": ", n = 2, simplify = TRUE)## [,1] [,2]
## [1,] "Nombre" "Ana"
## [2,] "País" "México"
## [3,] "Edad" "29"
En lugar de dividir cadenas por patrones, podemos dividir por carácter, línea, oración y palabra usando boundary():
x <- "Esta es una oración. Esta es otra oración."
str_view_all(x, boundary("word"))str_split(x, " ")[[1]]## [1] "Esta" "es" "una" "oración." "Esta" "es"
## [7] "otra" "oración."
str_split(x, boundary("word"))[[1]]## [1] "Esta" "es" "una" "oración" "Esta" "es" "otra"
## [8] "oración"
str_locate() y str_locate_all() nos darán las posiciones inicial y final de cada coincidencia. Estos son particularmente útiles cuando ninguna de las otras funciones hace exactamente lo que deseamos. Podemos usar str_locate() para encontrar el patrón coincidente, str_sub() para extraerlo y/o modificarlo.
Cuando utilizamos un patrón que es una cadena, se envuelve automáticamente en una llamada a regex():
# La llamada regular:
str_view(fruit, "nana")
# Es una versión corta de:
str_view(fruit, regex("nana"))Podemos usar los otros argumentos de regex() para controlar los detalles de la coincidencia:
ignore_case = TRUE permite que los caracteres coincidan con sus mayúsculas o minúsculas. Esto siempre usa la configuración regional actual.bananas <- c("banana", "Banana", "BANANA")
str_view(bananas, "banana")str_view(bananas, regex("banana", ignore_case = TRUE))multiline = TRUE permite a ^ y $ coincidir el inicio y el final de cada línea en lugar del inicio y el final de la cadena completa.x <- "Line 1\nLine 2\nLine 3"
str_extract_all(x, "^Line")[[1]]## [1] "Line"
str_extract_all(x, regex("^Line", multiline = TRUE))[[1]]## [1] "Line" "Line" "Line"
comments = TRUE nos permite usar comentarios y espacios en blanco para hacer que las expresiones regulares complejas sean más comprensibles. Los espacios son ignorados, como lo es todo después #. Para que coincida con un espacio literal, necesitamos escaparlo: "\\ ".phone <- regex("
\\(? # optional opening parens
(\\d{3}) # area code
[) -]? # optional closing parens, space, or dash
(\\d{3}) # another three numbers
[ -]? # optional space or dash
(\\d{3}) # three more numbers
", comments = TRUE)
str_match("514-791-8141", phone)## [,1] [,2] [,3] [,4]
## [1,] "514-791-814" "514" "791" "814"
dotall = TRUE permite que . coincida con todo, incluso \n.Hay otras tres funciones que podemos usar en lugar de regex():
fixed(): coincide exactamente con la secuencia de bytes especificada. Ignora todas las expresiones regulares especiales y opera a un nivel muy bajo. Esto nos permite evitar el escape complejo y puede ser mucho más rápido que las expresiones regulares. El siguiente microbenchmark muestra que es aproximadamente 3 veces más rápido en un ejemplo simple.library(microbenchmark)
microbenchmark::microbenchmark(
fixed = str_detect(sentences, fixed("the")),
regex = str_detect(sentences, "the"),
times = 20
)Debemos tener cuidado al usar fixed() con datos que no sean en inglés. Es problemático porque a menudo hay múltiples formas de representar al mismo caracter. Por ejemplo, hay dos formas de definir "á": como un solo caracter o como una "a" más un acento:
a1 <- "\u00e1"
a2 <- "a\u0301"
c(a1, a2)## [1] "á" "á"
a1 == a2## [1] FALSE
Se representan de forma idéntica, pero debido a que están definidos de manera diferente, fixed() no encuentran una coincidencia. En cambio, podemos usar coll(), definido a continuación, para respetar las reglas de comparación de caracteres humanos:
str_detect(a1, fixed(a2))## [1] FALSE
str_detect(a1, coll(a2))## [1] TRUE
coll(): compara cadenas usando las reglas de colación estándar. Esto es útil para hacer coincidencias insensibles a mayúscuas y minúsculas. Notemos que coll() toma un parámetro locale que controla qué reglas se usan para comparar caracteres. Desafortunadamente, ¡diferentes partes del mundo usan diferentes reglas!# Esto significa que debemos estar conscientes de las diferencias cuando hagamos coincidencias con sensibilidad de mayúsculas:
i <- c("I", "İ", "i", "ı")
i## [1] "I" "İ" "i" "ı"
str_subset(i, coll("i", ignore_case = TRUE))## [1] "I" "i"
str_subset(i, coll("i", ignore_case = TRUE, locale = "tr"))## [1] "İ" "i"
Tanto fixed() como regex() tienen argumentos ignore_case, pero no permiten que podamos elegir la configuración regional: siempre utilizan la configuración regional predeterminada. Podemos verla así:
stringi::stri_locale_info()## $Language
## [1] "en"
##
## $Country
## [1] "US"
##
## $Variant
## [1] ""
##
## $Name
## [1] "en_US"
La desventaja de coll() es la velocidad porque las reglas para reconocer qué caracteres son los mismos son complicadas, coll() es relativamente lento en comparación con regex() y fixed().
str_split() podemos usar boundary() para unir límites. También podemos usarlo con las otras funciones:x <- "Esta es una oración."
str_view_all(x, boundary("word"))str_extract_all(x, boundary("word"))## [[1]]
## [1] "Esta" "es" "una" "oración"
Hay dos funciones útiles en R base que también usan expresiones regulares:
apropos() busca todos los objetos disponibles del entorno global. Esto es útil si no recordamos el nombre de alguna función.apropos("replace")## [1] "replace" "%+replace%" "replace_na"
## [4] "setReplaceMethod" "str_replace" "str_replace_all"
## [7] "str_replace_na" "theme_replace"
dir() lista todos los archivos en un directorio. El argumento pattern toma una expresión regular y solo devuelve nombres de archivos que coinciden con el patrón. Por ejemplo, podemos encontrar todos los archivos R Markdown en el directorio actual con:head(dir(pattern = "\\.Rmd$"))## [1] "3-AT4A.Rmd"
(Si te sientes más cómodo con “globs” como *.Rmd, puedes convertirlos a expresiones regulares con glob2rx()):
stringr está construido sobre el paquete stringi. stringr es útil cuando estamos aprendiendo porque expone un conjunto mínimo de funciones, que se han seleccionado cuidadosamente para manejar las funciones de manipulación de cadenas más comunes. stringi, por otro lado, está diseñado para ser completo. Contiene casi todas las funciones que podamos necesitar: stringi tiene 234 funciones contra las 46 de stringr.
Si te encuentras luchando por hacer algo en stringr, vale la pena echarle un vistazo a stringi. Los paquetes funcionan de manera muy similar, por lo que deberías poder traducir el conocimiento de forma natural. La principal diferencia es el prefijo: str_ vs stri_.
En R, los factores se usan para trabajar con variables categóricas, variables que tienen un conjunto fijo y conocido de valores posibles. También son útiles cuando queremos mostrar vectores de caracteres en un orden no alfabético.
Históricamente, los factores eran mucho más fáciles de trabajar que los caracteres. Como resultado, muchas de las funciones de R base automáticamente convierten los caracteres en factores. Esto significa que los factores a menudo surgen en lugares donde no son realmente útiles.
Para trabajar con factores, usaremos el paquete forcats, que proporciona herramientas para trabajar con variables categóricas. Proporciona una amplia gama de ayudantes para trabajar con factores. forcats no es parte del tidyverse central, por lo que debemos cargarlo explícitamente.
library(tidyverse)
library(forcats)Imaginemos que tenemos una variable que registra el mes:
x1 <- c("Dec", "Apr", "Jan", "Mar")Usar una cadena para registrar esta variable tiene dos problemas:
x2 <- c("Dec", "Apr", "Jam", "Mar")sort(x1)## [1] "Apr" "Dec" "Jan" "Mar"
Podemos solucionar ambos problemas con un factor. Para crear un factor, debemos comenzar creando una lista de los niveles válidos:
month_levels <- c(
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
)Ahora podemos crear un factor:
y1 <- factor(x1, levels = month_levels)
y1## [1] Dec Apr Jan Mar
## Levels: Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec
sort(y1)## [1] Jan Mar Apr Dec
## Levels: Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec
Y cualquier valor que no esté en el conjunto se convertirá silenciosamente a NA:
y2 <- factor(x2, levels = month_levels)
y2## [1] Dec Apr <NA> Mar
## Levels: Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec
Si omitimos los niveles, se tomarán de los datos en orden alfabético:
factor(x1)## [1] Dec Apr Jan Mar
## Levels: Apr Dec Jan Mar
A veces preferiremos que el orden de los niveles coincida con el orden de la primera aparición en los datos. Podemos hacerlo al crear el factor estableciendo niveles a unique(x), o después del factor, con fct_inorder():
f1 <- factor(x1, levels = unique(x1))
f1## [1] Dec Apr Jan Mar
## Levels: Dec Apr Jan Mar
f2 <- x1 %>% factor() %>% fct_inorder()
f2## [1] Dec Apr Jan Mar
## Levels: Dec Apr Jan Mar
Si alguna vez necesitamos acceder al conjunto de niveles válidos directamente, podemos hacerlo con levels():
levels(f2)## [1] "Dec" "Apr" "Jan" "Mar"
A menudo es útil cambiar el orden de los niveles de los factores en una visualización. Por ejemplo, imagina que queremos explorar el promedio de horas dedicadas a ver televisión por día en diferentes religiones:
relig_summary <- gss_cat %>%
group_by(relig) %>%
summarise(
age = mean(age, na.rm = TRUE),
tvhours = mean(tvhours, na.rm = TRUE),
n = n()
)
ggplot(relig_summary, aes(tvhours, relig)) + geom_point()Es difícil interpretar esta gráfica porque no hay un patrón general. Podemos mejorarlo reordenando los niveles de relig usando fct_reorder(). fct_reorder() toma tres argumentos:
f, el factor cuyos niveles queremos modificarx, un vector numérico que deseamos usar para reordenar los niveles.fun una función que se usa si hay múltiples valores de x para cada valor de f. El valor predeterminado es median.ggplot(relig_summary, aes(tvhours, fct_reorder(relig, tvhours))) +
geom_point()Reordenar la religión hace que sea mucho más fácil ver que las personas de la categoría “No sé” ven mucha más televisión, y el Hinduismo y otras religiones orientales ven mucho menos.
A medida que comenzamos a hacer transformaciones más complicadas, es recomendable moverlas de aes() a un mutate() por separado. Por ejemplo, podríamos reescribir la gráfica anterior como:
relig_summary %>%
mutate(relig = fct_reorder(relig, tvhours)) %>%
ggplot(aes(tvhours, relig)) +
geom_point()¿Qué pasa si creamos una gráfica similar mirando cómo varía la edad promedio a través del nivel de ingresos reportados?
rincome_summary <- gss_cat %>%
group_by(rincome) %>%
summarise(
age = mean(age, na.rm = TRUE),
tvhours = mean(tvhours, na.rm = TRUE),
n = n()
)
ggplot(rincome_summary, aes(age, fct_reorder(rincome, age))) + geom_point()¡Aquí, reordenar arbitrariamente los niveles no es una buena idea! Eso es porque rincome ya tiene un orden principal con el que no deberíamos meternos. Reservemos fct_reorder() para factores cuyos niveles se ordenan arbitrariamente.
Sin embargo, tiene sentido tirar “No aplicable” al frente con los otros niveles especiales. Podemos usar fct_relevel(). Toma un factor, f y luego cualquier cantidad de niveles que deseemos mover al frente de la línea.
ggplot(rincome_summary,
aes(age, fct_relevel(rincome, "Not applicable")))
+ geom_point()¿Por qué crees que la edad promedio para “No aplicable” es tan alta?
Otro tipo de reordenamiento es útil cuando se colorean las líneas en un diagrama. fct_reorder2() reordena el factor por los valores y asociados con los valores x más grandes. Esto hace que la gráfica sea más fácil de leer porque los colores de la línea se alinean con la leyenda.
by_age <- gss_cat %>%
filter(!is.na(age)) %>%
count(age, marital) %>%
group_by(age) %>%
mutate(prop = n / sum(n))
ggplot(by_age, aes(age, prop, colour = marital)) +
geom_line(na.rm = TRUE)ggplot(by_age,
aes(age, prop, colour = fct_reorder2(marital, age, prop))) +
geom_line() +
labs(colour = "marital")Finalmente, para gráficas de barra, podemos usar fct_infreq() para ordenar niveles en frecuencia creciente: este es el tipo más simple de reordenamiento porque no necesita ninguna variable adicional. Es posible que necesitemos combinar con fct_rev().
gss_cat %>%
mutate(marital = marital %>% fct_infreq() %>% fct_rev()) %>%
ggplot(aes(marital)) +
geom_bar()Más poderoso que cambiar las órdenes de los niveles es cambiar sus valores. Esto nos permite aclarar las etiquetas para nuestra publicación y los niveles de contracción para las pantallas de alto nivel. La herramienta más general y poderosa es fct_recode(). Nos permite recodificar o cambiar el valor de cada nivel. Por ejemplo, tomemos el gss_cat$partyid:
gss_cat %>% count(partyid)Los niveles son escuetos e inconsistentes. Vamos a ajustarlos para que sean más largos y usar una construcción paralela.
gss_cat %>%
mutate(partyid = fct_recode(partyid,
"Republican, strong" = "Strong republican",
"Republican, weak" = "Not str republican",
"Independent, near rep" = "Ind,near rep",
"Independent, near dem" = "Ind,near dem",
"Democrat, weak" = "Not str democrat",
"Democrat, strong" = "Strong democrat"
)) %>%
count(partyid)fct_recode() dejará los niveles que no se mencionan explícitamente tal como están y nos advertirá si nos estamos refiriendo accidentalmente a un nivel que no existe.
Para combinar grupos, podemos asignar múltiples niveles antiguos al mismo nivel nuevo:
gss_cat %>%
mutate(partyid = fct_recode(partyid,
"Republican, strong" = "Strong republican",
"Republican, weak" = "Not str republican",
"Independent, near rep" = "Ind,near rep",
"Independent, near dem" = "Ind,near dem",
"Democrat, weak" = "Not str democrat",
"Democrat, strong" = "Strong democrat",
"Other" = "No answer",
"Other" = "Don't know",
"Other" = "Other party"
)) %>%
count(partyid)Debemos usar esta técnica con cuidado: si agrupamos categorías que son realmente diferentes, terminaremos con resultados engañosos.
Si queremos colapsar muchos niveles, fct_collapse() es una variante útil de fct_recode(). Para cada nueva variable, podemos proporcionar un vector de niveles antiguos:
gss_cat %>%
mutate(partyid = fct_collapse(partyid,
other = c("No answer", "Don't know", "Other party"),
rep = c("Strong republican", "Not str republican"),
ind = c("Ind,near rep", "Independent", "Ind,near dem"),
dem = c("Not str democrat", "Strong democrat")
)) %>%
count(partyid)A veces solo queremos agrupar a todos los grupos pequeños para simplificar un diagrama o una tabla. Ese es el trabajo de fct_lump():
gss_cat %>%
mutate(relig = fct_lump(relig)) %>%
count(relig)El comportamiento predeterminado es agrupar progresivamente los grupos más pequeños, asegurando que el agregado sigue siendo el grupo más pequeño. En este caso no es muy útil: es cierto que la mayoría de los estadounidenses en esta encuesta son protestantes, pero es probable colapsemos de más.
En cambio, podemos usar el parámetro n para especificar cuántos grupos (excluyendo otros) queremos mantener:
gss_cat %>%
mutate(relig = fct_lump(relig, n = 10)) %>%
count(relig, sort = TRUE) %>%
print(n = Inf)## # A tibble: 10 x 2
## relig n
## <fct> <int>
## 1 Protestant 10846
## 2 Catholic 5124
## 3 None 3523
## 4 Christian 689
## 5 Other 458
## 6 Jewish 388
## 7 Buddhism 147
## 8 Inter-nondenominational 109
## 9 Moslem/islam 104
## 10 Orthodox-christian 95
A primera vista, las fechas y las horas parecen simples. Los usamos todo el tiempo en la vida normal, y no parecen causar mucha confusión. Sin embargo, cuanto más aprendes sobre fechas y horarios, más complicados parecen obtenerlos. Para calentar, probemos estas tres preguntas aparentemente simples:
Sabemos que no todos los años tienen 365 días, pero ¿conoces la regla completa para determinar si un año es bisiesto? Es posible que recordemos que muchas partes del mundo usan el horario de verano (DST), por lo que algunos días tienen 23 horas y otros tienen 25. Es posible que no sepamos que algunos minutos tienen 61 segundos porque de vez en cuando se agregan segundos intercalares porque la rotación de la Tierra se está desacelerando gradualmente.
Las fechas y los tiempos son difíciles porque tienen que conciliar dos fenómenos físicos (la rotación de la Tierra y su órbita alrededor del sol) con una gran cantidad de fenómenos geopolíticos que incluyen meses, zonas horarias y horario de verano.
Usaremos el paquete lubridate, que facilita el trabajo con fechas y horas en R. lubridate no forma parte del tidyverse. Usaremos la base que ya conocemos nycflights13 para practicar.
library(tidyverse)
library(lubridate)
library(nycflights13)Hay tres tipos de datos de fecha/hora que se refieren a un instante en el tiempo:
Una fecha. Tibbles imprime esto como <date>.
Una hora del día. Tibbles imprime esto como <time>.
Una fecha-hora es una fecha más una hora: identifica de manera única un instante en el tiempo (generalmente al segundo más cercano). Tibbles imprime esto como <dttm>.
Siempre debemos usar el tipo de datos más simple posible que funcione para nuestras necesidades. Eso significa que si podemos usar una fecha en lugar de una fecha-hora, deberíamos hacerlo. Los horarios son sustancialmente más complicados debido a la necesidad de manejar zonas horarias.
Para obtener la fecha actual o la fecha-hora, usamos today() o now():
today()## [1] "2018-07-21"
now()## [1] "2018-07-21 09:55:57 CDT"
De lo contrario, hay tres formas en las que es probable que creemos una fecha/hora:
Funcionan de la siguiente manera.
Los datos de fecha/hora a menudo vienen como cadenas. Podemos usar los ayudantes provistos por lubridate para transformarlas en formato de fecha-hora. Automáticamente resuelven el formato una vez que especificamos el orden del componente. Para usarlos, debemos identificar el orden en que aparecen el año, el mes y el día en las fechas, luego organizar “y”, “m” y “d” en el mismo orden. Eso nos da el nombre de la función de lubridate que parseara la fecha. Por ejemplo:
ymd("2017-01-31")## [1] "2017-01-31"
mdy("January 31st, 2017")## [1] NA
dmy("31-Jan-2017")## [1] "2017-01-31"
Estas funciones también toman números sin comillas. Esta es la forma más concisa de crear un único objeto de fecha/hora, como puede ser necesario al filtrar datos de fecha/hora. ymd() es corto e inequívoco:
ymd(20170131)## [1] "2017-01-31"
ymd() y sus amigos crean fechas. Para crear una fecha y hora, agregamos un guión bajo y una o más de “h”, “m” y “s” al nombre de la función de análisis sintáctico:
ymd_hms("2017-01-31 20:11:59")## [1] "2017-01-31 20:11:59 UTC"
mdy_hm("01/31/2017 08:01")## [1] "2017-01-31 08:01:00 UTC"
También podemos forzar la creación de una fecha y hora a partir de una fecha proporcionando una zona horaria:
ymd(20170131, tz = "UTC")## [1] "2017-01-31 UTC"
En lugar de una sola cadena, a veces tendremos los componentes individuales de la fecha y hora distribuidos en varias columnas. Esto es lo que tenemos en los datos de vuelos:
flights %>%
select(year, month, day, hour, minute)Para crear una fecha/hora a partir de este tipo de entrada, usamos make_date() para fechas o make_datetime() fechas-horas:
flights %>%
select(year, month, day, hour, minute) %>%
mutate(departure = make_datetime(year, month, day, hour, minute))Hagamos lo mismo para cada una de las cuatro columnas de tiempo en flights. Los tiempos están representados en un formato ligeramente extraño, por lo que usamos la aritmética de módulo para extraer los componentes de hora y minuto.
make_datetime_100 <- function(year, month, day, time) {
make_datetime(year, month, day, time %/% 100, time %% 100)
}
flights_dt <- flights %>%
filter(!is.na(dep_time), !is.na(arr_time)) %>%
mutate(
dep_time = make_datetime_100(year, month, day, dep_time),
arr_time = make_datetime_100(year, month, day, arr_time),
sched_dep_time = make_datetime_100(year, month, day, sched_dep_time),
sched_arr_time = make_datetime_100(year, month, day, sched_arr_time)
) %>%
select(origin, dest, ends_with("delay"), ends_with("time"))
flights_dtCon estos datos, podemos visualizar la distribución de los horarios de salida a lo largo del año:
flights_dt %>%
ggplot(aes(dep_time)) +
geom_freqpoly(binwidth = 86400) # 86400 segundos = 1 díaO en un solo día:
flights_dt %>%
filter(dep_time < ymd(20130102)) %>%
ggplot(aes(dep_time)) +
geom_freqpoly(binwidth = 600) # 600 s = 10 minutosNotemos que cuando utilizamos fechas y horas en un contexto numérico (como en un histograma), 1 significa 1 segundo, por lo que un ancho de 86400 equivale a un día. Para las fechas, 1 significa 1 día.
Es posible que deseemos cambiar entre una fecha-hora y una fecha. Para eso, usamos as_datetime() y as_date():
as_datetime(today())## [1] "2018-07-21 UTC"
as_date(now())## [1] "2018-07-21"
Algunas veces obtendremos fecha/hora como compensaciones numéricas del “Unix Epoch”, 1970-01-01. Si el desplazamiento está en segundos, usamos as_datetime() si es en días, usamos as_date().
as_datetime(60 * 60 * 10)## [1] "1970-01-01 10:00:00 UTC"
as_date(365 * 10 + 2)## [1] "1980-01-01"
Ahora que sabemos cómo obtener datos de fecha y hora en las estructuras de datos de fecha y hora de R, exploremos qué podemos hacer con ellos.
Podemos sacar las piezas individuales de la fecha con las funciones de acceso year(), month(), mday() (día del mes), yday() (los días del año), wday() (día de la semana), hour(), minute(), y second().
datetime <- ymd_hms("2016-07-08 12:34:56")
year(datetime)## [1] 2016
month(datetime)## [1] 7
mday(datetime)## [1] 8
yday(datetime)## [1] 190
wday(datetime)## [1] 6
Para month() y wday() podemos configurar label = TRUE para devolver el nombre abreviado del mes o el día de la semana. También podemos establecer abbr = FALSE para devolver el nombre completo.
month(datetime, label = TRUE)## [1] Jul
## 12 Levels: Jan < Feb < Mar < Apr < May < Jun < Jul < Aug < Sep < ... < Dec
wday(datetime, label = TRUE, abbr = FALSE)## [1] Friday
## 7 Levels: Sunday < Monday < Tuesday < Wednesday < Thursday < ... < Saturday
Podemos utilizar wday() para ver que más vuelos salen durante la semana que en el fin de semana:
flights_dt %>%
mutate(wday = wday(dep_time, label = TRUE)) %>%
ggplot(aes(x = wday)) +
geom_bar()Hay un patrón interesante si observamos el retraso promedio de salida por minuto dentro de una hora. ¡Parece que los vuelos que salen en minutos 20-30 y 50-60 tienen retrasos mucho más bajos que el resto de la hora!
flights_dt %>%
mutate(minute = minute(dep_time)) %>%
group_by(minute) %>%
summarise(
avg_delay = mean(arr_delay, na.rm = TRUE),
n = n()) %>%
ggplot(aes(minute, avg_delay)) +
geom_line()Curiosamente, si vemos la hora programada de salida, no vemos un patrón tan fuerte:
sched_dep <- flights_dt %>%
mutate(minute = minute(sched_dep_time)) %>%
group_by(minute) %>%
summarise(
avg_delay = mean(arr_delay, na.rm = TRUE),
n = n())
ggplot(sched_dep, aes(minute, avg_delay)) +
geom_line()Entonces, ¿por qué vemos ese patrón con los tiempos reales de salida? Bueno, al igual que muchos datos recopilados por humanos, hay un fuerte sesgo hacia los vuelos que salen en tiempos de salida “agradables”. Siempre debemos estar atentos a este tipo de patrón cuando trabajemos con datos que involucren el juicio humano.
ggplot(sched_dep, aes(minute, n)) +
geom_line()Un enfoque alternativo para graficar los componentes individuales es redondear la fecha a una unidad cercana de tiempo, con floor_date(), round_date(), y ceiling_date(). Cada función toma un vector de fechas para ajustar y redondear. Esto, por ejemplo, nos permite graficar el número de vuelos por semana:
flights_dt %>%
count(week = floor_date(dep_time, "week")) %>%
ggplot(aes(week, n)) +
geom_line()También podemos usar cada función para configurar los componentes de una fecha/hora:
(datetime <- ymd_hms("2016-07-08 12:34:56"))## [1] "2016-07-08 12:34:56 UTC"
year(datetime) <- 2020
datetime## [1] "2020-07-08 12:34:56 UTC"
month(datetime) <- 01
datetime## [1] "2020-01-08 12:34:56 UTC"
hour(datetime) <- hour(datetime) + 1
datetime## [1] "2020-01-08 13:34:56 UTC"
Alternativamente, en lugar de modificar, podemos crear una nueva fecha y hora con update(). Esto también nos permite establecer múltiples valores a la vez.
update(datetime, year = 2020, month = 2, mday = 2, hour = 2)## [1] "2020-02-02 02:34:56 UTC"
Si los valores son demasiado grandes, se reiniciarán:
ymd("2015-02-01") %>%
update(mday = 30)## [1] "2015-03-02"
ymd("2015-02-01") %>%
update(hour = 400)## [1] "2015-02-17 16:00:00 UTC"
Podemos usar update() para mostrar la distribución de vuelos a lo largo del día para cada día del año:
flights_dt %>%
mutate(dep_hour = update(dep_time, yday = 1)) %>%
ggplot(aes(dep_hour)) +
geom_freqpoly(binwidth = 300)Veremos tres clases importantes que representan períodos de tiempo:
En R, cuando restamos dos fechas, obtenemos un objeto de tiempo difftime:
age <- today() - ymd(19880813)
age## Time difference of 10934 days
Un objeto de clase difftime registra un lapso de tiempo de segundos, minutos, horas, días o semanas. Esta ambigüedad puede hacer que sea un poco doloroso trabajar con él, por lo que lubridate proporciona una alternativa que siempre usa segundos: la duración.
as.duration(age)## [1] "944697600s (~29.94 years)"
Las duraciones vienen con un grupo de constructores convenientes:
dseconds(15)## [1] "15s"
dminutes(10)## [1] "600s (~10 minutes)"
dhours(c(12, 24))## [1] "43200s (~12 hours)" "86400s (~1 days)"
ddays(0:5)## [1] "0s" "86400s (~1 days)" "172800s (~2 days)"
## [4] "259200s (~3 days)" "345600s (~4 days)" "432000s (~5 days)"
dweeks(3)## [1] "1814400s (~3 weeks)"
dyears(1)## [1] "31536000s (~52.14 weeks)"
Las duraciones siempre registran el lapso de tiempo en segundos. Las unidades más grandes se crean mediante la conversión de minutos, horas, días, semanas y años a segundos a las tasas estándar (60 segundos en un minuto, 60 minutos en una hora, 24 horas en día, 7 días a la semana, los 365 días en un año).
Podemos sumar y multiplicar duraciones:
2 * dyears(1)## [1] "63072000s (~2 years)"
dyears(1) + dweeks(12) + dhours(15)## [1] "38847600s (~1.23 years)"
Podemos sumar y restar duraciones desde y a días:
tomorrow <- today() + ddays(1)
last_year <- today() - dyears(1)Sin embargo, dado que las duraciones representan una cantidad exacta de segundos, a veces podemos obtener un resultado inesperado:
one_pm <- ymd_hms("2016-03-12 13:00:00", tz = "America/New_York")
one_pm## [1] "2016-03-12 13:00:00 EST"
one_pm + ddays(1)## [1] "2016-03-13 14:00:00 EDT"
¿Por qué un día después de la 1 pm el 12 de marzo no da las 2 pm el 13 de marzo? Si observamos detenidamente la fecha, también podemos notar que las zonas horarias han cambiado. Debido al horario de verano, el 12 de marzo solo tiene 23 horas, por lo que si agregamos días en segundos, terminamos con un horario diferente.
Para resolver este problema, lubridate proporciona periodos. Los períodos son períodos de tiempo, pero no tienen una duración fija en segundos, sino que funcionan con tiempos “humanos”, como días y meses. Eso les permite trabajar de una manera más intuitiva:
one_pm## [1] "2016-03-12 13:00:00 EST"
one_pm + days(1)## [1] "2016-03-13 13:00:00 EDT"
Al igual que las duraciones, los períodos se pueden crear con varias funciones amigables.
seconds(15)## [1] "15S"
minutes(10)## [1] "10M 0S"
hours(c(12, 24))## [1] "12H 0M 0S" "24H 0M 0S"
days(7)## [1] "7d 0H 0M 0S"
months(1:6)## [1] "1m 0d 0H 0M 0S" "2m 0d 0H 0M 0S" "3m 0d 0H 0M 0S" "4m 0d 0H 0M 0S"
## [5] "5m 0d 0H 0M 0S" "6m 0d 0H 0M 0S"
weeks(3)## [1] "21d 0H 0M 0S"
years(1)## [1] "1y 0m 0d 0H 0M 0S"
Podemos sumar y multiplicar periodos:
10 * (months(6) + days(1))## [1] "60m 10d 0H 0M 0S"
days(50) + hours(25) + minutes(2)## [1] "50d 25H 2M 0S"
Y, por supuesto, sumarlos a las fechas. En comparación con las duraciones, los períodos tienen más probabilidades de hacer lo que esperamos:
# Un año bisiesto
ymd("2016-01-01") + dyears(1)## [1] "2016-12-31"
ymd("2016-01-01") + years(1)## [1] "2017-01-01"
# Horario de Verano
one_pm + ddays(1)## [1] "2016-03-13 14:00:00 EDT"
one_pm + days(1)## [1] "2016-03-13 13:00:00 EDT"
Usemos períodos para arreglar una rareza relacionada con nuestras fechas de vuelo. Algunos aviones parecen haber llegado a su destino antes de partir de la ciudad de Nueva York.
flights_dt %>%
filter(arr_time < dep_time)Estos son vuelos nocturnos. Utilizamos la misma información de fecha tanto para la hora de salida como para la hora de llegada, pero estos vuelos llegaron al día siguiente. Podemos solucionar esto añadiendo days(1) al tiempo de llegada de cada vuelo nocturno.
flights_dt <- flights_dt %>%
mutate(
overnight = arr_time < dep_time,
arr_time = arr_time + days(overnight * 1),
sched_arr_time = sched_arr_time + days(overnight * 1)
)Ahora todos nuestros vuelos obedecen las leyes de la física.
flights_dt %>%
filter(overnight, arr_time < dep_time)Es obvio lo que dyears(1) / ddays(365) debería regresar: uno, porque las duraciones siempre están representadas por una cantidad de segundos, y una duración de un año se define como 365 días en segundos.
¿Qué debería regresar years(1) / days(1)?
Si el año fue 2015 debería devolver 365, pero si fuera 2016, ¡debería devolver 366! No hay suficiente información para que lubridate brinde una única respuesta clara. Lo que hace en cambio es dar una estimación, con una advertencia:
years(1) / days(1)## estimate only: convert to intervals for accuracy
## [1] 365.25
Si queremos una medida más precisa, usamos un intervalo. Un intervalo es una duración con un punto de inicio, eso lo hace preciso para que podamos determinar exactamente cuánto tiempo es:
next_year <- today() + years(1)
(today() %--% next_year) / ddays(1)## [1] 365
Para saber cuántos períodos entran en un intervalo, debemos usar la división de enteros:
(today() %--% next_year) %/% days(1)## [1] 365
¿Cómo se elige entre duración, períodos e intervalos? Como siempre, elegimos la estructura de datos más simple que resuelva nuestro problema. Si solo nos importa el tiempo físico, usamos una duración; si necesitamos agregar tiempos humanos, usamos un período; si necesitamos saber cuánto tiempo dura un lapso en unidades humanas, usamos un intervalo.
Las operaciones aritméticas permitidas entre pares de clases de fecha/hora:
Las zonas horarias son un tema enormemente complicado debido a su interacción con entidades geopolíticas. Afortunadamente, no necesitamos profundizar en todos los detalles, ya que no son todos importantes para el análisis de datos, pero hay algunos desafíos que debemos enfrentar.
El primer desafío es que los nombres diarios de las zonas horarias tienden a ser ambiguos. Por ejemplo, los estadounidenses, están familiarizados con EST o Eastern Standard Time. Sin embargo, tanto Australia como Canadá también tienen EST. Para evitar confusiones, R usa las zonas horarias estándar internacionales de IANA. Estos usan un esquema de nombres consistente "/", típicamente en la forma "<continente>/<ciudad>" (hay algunas excepciones porque no todos los países se encuentran en un continente). Los ejemplos incluyen "America/New_York", "Europe/Paris" y "Pacific/Auckland".
Podemos descubrir lo que piensa R que es nuestra zona horaria actual con Sys.timezone():
Sys.timezone()## [1] "America/Mexico_City"
(Si R no sabe, obtendremos un NA.)
Vemos la lista completa de todos los nombres de zona horaria con OlsonNames():
length(OlsonNames())## [1] 606
head(OlsonNames())## [1] "Africa/Abidjan" "Africa/Accra" "Africa/Addis_Ababa"
## [4] "Africa/Algiers" "Africa/Asmara" "Africa/Asmera"
En R, la zona horaria es un atributo de la fecha y hora que solo controla la impresión. Por ejemplo, estos tres objetos representan el mismo instante en el tiempo:
(x1 <- ymd_hms("2015-06-01 12:00:00", tz = "America/New_York"))## [1] "2015-06-01 12:00:00 EDT"
(x2 <- ymd_hms("2015-06-01 18:00:00", tz = "Europe/Copenhagen"))## [1] "2015-06-01 18:00:00 CEST"
(x3 <- ymd_hms("2015-06-02 04:00:00", tz = "Pacific/Auckland"))## [1] "2015-06-02 04:00:00 NZST"
Podemos verificar que son el mismo tiempo restando:
x1 - x2## Time difference of 0 secs
x1 - x3## Time difference of 0 secs
A menos que se especifique lo contrario, lubridate siempre usa UTC. UTC (tiempo universal coordinado) es la zona horaria estándar utilizada por la comunidad científica y más o menos equivalente a su predecesor, GMT (Greenwich Mean Time). No tiene horario de verano, lo que la hace una representación conveniente para cálculos. Las operaciones que combinan fechas y horas, como c(), a menudo ignorarán la zona horaria. En ese caso, los horarios se mostrarán en nuestra zona horaria local:
x4 <- c(x1, x2, x3)
x4## [1] "2015-06-01 11:00:00 CDT" "2015-06-01 11:00:00 CDT"
## [3] "2015-06-01 11:00:00 CDT"
Podemos cambiar la zona horaria de dos maneras:
x4a <- with_tz(x4, tzone = "Australia/Lord_Howe")
x4a## [1] "2015-06-02 02:30:00 +1030" "2015-06-02 02:30:00 +1030"
## [3] "2015-06-02 02:30:00 +1030"
x4a - x4## Time differences in secs
## [1] 0 0 0
x4b <- force_tz(x4, tzone = "Australia/Lord_Howe")
x4b## [1] "2015-06-01 11:00:00 +1030" "2015-06-01 11:00:00 +1030"
## [3] "2015-06-01 11:00:00 +1030"
x4b - x4## Time differences in hours
## [1] -15.5 -15.5 -15.5
A menudo verás paste() y paste0(). ¿Cuál es la diferencia entre las dos funciones? ¿A qué función de stringr son equivalentes? ¿Cómo difieren las funciones en su manejo de NA?
En tus propias palabras, describe la diferencia entre los argumentos sep y collapse de str_c().
Usa str_length() y str_sub() para extraer el caracter medio de una cadena. ¿Qué harás si la cadena tiene un número par de caracteres?
¿Qué hace str_wrap()? ¿Cuándo podrías querer usarlo?
¿Qué hace str_trim()? ¿Qué es lo opuesto a str_trim()?
Escribe una función que convierta (por ejemplo) un vector c("a", "b", "c") en la cadena a, b y c. Piensa con cuidado sobre lo que debería hacer esta función si recibe un vector de longitud 0, 1 o 2.
Explica por qué cada una de estas cadenas no coinciden con un \: "\", "\\", "\\\".
¿Cómo coincidirías la secuencia "'\?
¿Qué patrones encontrará la expresión regular \..\..\..? ¿Cómo lo representarías como una cadena?
¿Cómo coincidirías con la cadena literal "$^$"?
Dado el corpus de palabras en stringr::words, crea expresiones regulares que encuentren todas las palabras que:
Dado que esta lista es larga, es posible que desees utilizar el argumento match de str_view() para mostrar solo las palabras coincidentes o no coincidentes.
Crea expresiones regulares para encontrar todas las palabras que:
ed, pero no con eed.ing o ise.Crea una expresión regular que coincida con los números de teléfono escritos comúnmente en tu país.
Describe los equivalentes de ?, +, * en la forma {m,n}.
Crea expresiones regulares para encontrar todas las palabras que:
Construya expresiones regulares para unir palabras que:
Para cada uno de los siguientes desafíos, intenta resolverlo utilizando una sola expresión regular y una combinación de llamadas múltiples a str_detect().
x.¿Qué palabra tiene el mayor número de vocales? ¿Qué palabra tiene la mayor proporción de vocales? (Sugerencia: ¿cuál es el denominador?)
Encuentra todas las palabras que vienen después de un “número” como “uno”, “dos”, “tres”, etc. Saca el número y la palabra (en inglés).
Encuentra todas las contracciones. Separa las piezas antes y después del apóstrofe.
Reemplaza todas las barras diagonales en una cadena con barras diagonales inversas.
Implementa una versión simple de str_to_lower() usando replace_all().
Cambia la primera y la última letra en words. ¿Cuáles de esas cadenas son todavía palabras?
Divide una cadena como “apples, pears, and bananas” en componentes individuales.
¿Por qué es mejor dividir con boundary("word") que con " "?
¿Qué hace la división con una cadena vacía ("")? Experimenta y luego lee la documentación.
¿Cuáles son las cinco palabras más comunes en sentences?
Encuentra las funciones de stringi que:
¿Cómo controlas el lenguaje que stri_sort() usa para ordenar?
Explora la distribución de rincome en forcats::gss_cat (ingreso reportado). ¿Qué hace que la gráfica de barras predeterminada sea difícil de entender? ¿Cómo podrías mejorarla?
¿Cuál es el más común relig en esta encuesta? ¿Cuál es el más común partyid?
Hay algunos números sospechosamente altos en tvhours. ¿Es la media un buen resumen?
Para cada factor en gss_cat identifica si el orden de los niveles es arbitrario o principal.
¿Por qué mover “No aplicable” de rincome al frente de los niveles lo mueve al final de la gráfica?
¿Cómo han cambiado con el tiempo las proporciones de personas que se identifican como demócratas, republicanos e independientes?
¿Cómo podrías colapsar rincome en un pequeño conjunto de categorías?
ymd(c("2010-10-10", "bananas"))¿Qué hace el argumento tzone para today()? ¿Por qué es importante?
Use la función de lubridate apropiada para parsear cada una de las siguientes fechas:
d1 <- "January 1, 2010"
d2 <- "2015-Mar-07"
d3 <- "06-Jun-2017"
d4 <- c("August 19 (2015)", "July 1 (2015)")
d5 <- "12/30/14" # 30 Dic, 2014¿Cómo cambia la distribución de los tiempos de vuelo en un día a lo largo del año?
Compara dep_time, sched_dep_time y dep_delay. ¿Son consistentes? Explica tus hallazgos
Compara air_time con la duración entre la partida y la llegada. Explica tus hallazgos (Sugerencia: considera la ubicación del aeropuerto).
¿Cómo cambia el tiempo promedio de demora en el transcurso de un día? ¿Deberías usar dep_time o sched_dep_time? ¿Por qué?
¿En qué día de la semana deberías irte si quieres minimizar la posibilidad de un retraso?
¿Qué hace la distribución de diamonds$carat y flights$sched_dep_time similares?
Confirma la hipótesis de que las salidas anticipadas de los vuelos en los minutos 20-30 y 50-60 son causadas por vuelos programados que salen temprano. Sugerencia: crea una variable binaria que te diga si un vuelo se retrasó o no.
¿Cómo explicarías days(overnight * 1) a alguien que acaba de comenzar a aprender R? ¿Cómo funciona?
Crea un vector de fechas para el primer día de cada mes en 2015. Crea un vector de fechas que indique el primer día de cada mes en el año actual.
Escribe una función que, dada tu fecha de cumpleaños (como fecha), devuelva la edad que tienes en años.
¿Por qué no funciona (today() %--% (today() + years(1)) / months(1)?